/*
    Xml.cpp
     Created on: Jan 3, 2009
         Author: sez
 */

#include "Xml.h"
#include "XmlException.h"
#include "XmlFileNotFound.h"
#include "XmlTagNotFound.h"
#include "XmlPath.h"

#include <QDomDocument>
#include <QSettings>
#include <QFile>
#include <QDir>
#include <QDebug>


/*!
    \class Xml
    \brief Class to encapsulate Xml serializing to the filesystem.
    \details Manages all aspects of serializing data items to the filesystem
    including file paths, type conversion, and array type handling.
    Classes and user defined types should still know the names of fields
    in the data type.
    Generally create an XML object initialized with the types of the data
    items it will be serializing.  This determines what directory in the file
    system it will be stored in.
    \code
    Xml xml("Lizardman", DndData::RACIAL_BASE);
    \endcode
    Then call templated methods to add data to the xml object.  The data types
    must have operator << and >> defined for text streams.  The types can be
    single instances, or QList's of instances.
    \code
    appendTag(xml, "TongueLength", 55);
    QList<QString> habits;
    habits << "foraging" << "slithering";
    appendArray(xml, habits);
    \endcode
    Finally save to the file system.
    \code
    xml.serialize();
    \endcode
    The static methods Xml::getDataRootDir() and Xml::getDataDir() return
    QDir instances that can be used to query the file system about files stored.
    Note that in general Xml methods will throw either XmlException or XmlFileNotFound
    exceptions in case of exceptional conditions.
 */

/*!
    \brief Construct a new Xml object.
    \details The new Xml object models a data item with \a name, \a type
    and optionally \a subType as given.  These values will also determine where in
    the Xml data directory heierarchy the Xml file is stored when serialized or
    deserialized.
 */
Xml::Xml(const XmlPath &xp)
    : mDoc(new QDomDocument(xp.typeName()))
    , mXmlPath(0)
{
    QDomElement root = mDoc->createElement(xp.typeName());
    mDoc->appendChild(root);
    mXmlPath = new XmlPath(xp);
}

/*!
    \brief Destroy the Xml object.
    \details Return any resources used.
 */
Xml::~Xml()
{
    delete mXmlPath;
    delete mDoc;
}

/*!
    \brief Return the file path for this Xml data object.
    \details Based on the objects name, type (and possible sub-type) determine
    the file path, and return it as a QString.  No check for existence is made.
    \return QString of the file path.
    \throw XmlException() if the type or sub-type directories did not exist and
    could not be created - ie, this calls getDataDir().
 */
QString Xml::getDataFileName() const
{
    return mXmlPath->path();
}

/*!
    \brief Append an Xml tag \a tagName containing \a data.
    \param tagName the name of the data element to append.
    \param data the QString value of the data to append.
 */
void Xml::appendTag(const QString &tagName, const QString &data)
{
    QDomElement root = mDoc->documentElement();
    QDomElement tag = mDoc->createElement(tagName);
    root.appendChild(tag);
    QDomText text = mDoc->createTextNode(data);
    tag.appendChild(text);
}

/*!
    \brief Append a set of Xml tags containing the \a data.
    \details Create a parent tag \a tagName containing a set of element
    tags, each holding one item of \a data.
    \param tagName to contain the set of tags.
    \param data to store in the tags.
 */
void Xml::appendArray(const QString &tagName, const QStringList &data)
{
    QDomElement root = mDoc->documentElement();
    QDomElement tag = mDoc->createElement(tagName);
    root.appendChild(tag);
    for (int i = 0; i < data.size(); ++i)
    {
        QDomElement cell = mDoc->createElement("value");
        tag.appendChild(cell);
        QDomText text = mDoc->createTextNode(data.at(i));
        cell.appendChild(text);
    }
}

/*!
    \brief Retrieve the stringified value for the given \a key.
    \details Queries the xml document underlying for the key, which must
    be unique in the document, and returns the string data associated.
    Generally instead of calling this method directly, use
    the templated convenience function value<T>(Xml&, const QString &) to
    retrieve the typed data, passing in this xml object as a parameter.
    \code
    double myObj.legLength = value<double>(xml, key);
    \endcode
    \throw An XmlException() is thrown if either the tag is not known,
    or there are multiple tags called \a key.
    \return QString of the stringified value.
 */
QString Xml::value(const QString &key) const
{
    QDomNodeList items = mDoc->elementsByTagName(key);
    if (items.size() == 0)
        throw XmlTagNotFound(key, mXmlPath->path());
    if (items.size() > 1)
        throw XmlException(QObject::tr(
                "Multiple tags \"%1\" in %2").arg(
                        key).arg(mXmlPath->path()));
    QDomElement element = items.at(0).toElement();
    return element.text();
}

/*!
    \brief Retrieve the array of stringified values for the \a key.
    \details Queries the xml document for the \a key, which must be
    unique in the document, and returns a list of the data items
    associated, in string serialized form.
    Generally instead of calling this method directly, use
    the templated convenience function valueArray<T>(Xml&, const QString &) to
    retrieve the typed data, passing in this xml object as a parameter.
    \code
    double myObj.languages = valueArray<DndData::Language>(xml, key);
    \endcode
    \return QStringList of the stringified values.
    \throw XmlException() is thrown if either the tag is not known, or there
    are multiple tags called \a key.
 */
QStringList Xml::valueArray(const QString &key) const
{
    QDomNodeList items = mDoc->elementsByTagName(key);
    if (items.size() == 0)
        throw XmlTagNotFound(key, mXmlPath->path());
    if (items.size() > 1)
        throw XmlException(
                QString("Multiple tags \"%1\" in %2").arg(
                        key).arg(mXmlPath->path()));
    QDomElement element = items.at(0).toElement();
    QDomNodeList array = element.childNodes();
    QStringList results;
    for (int index = 0; index < array.size(); ++index)
    {
        if (!array.at(index).isElement())
            continue;
        QDomElement node = array.at(index).toElement();
        results << node.text();
    }
    return results;
}

/*!
    \brief Serialize the Xml coded data to the file system.
    \details Once this method successfully returns the Xml coded
    data in it has been serialized to a file, and can be retrieved
    via deserializeXml().
    \throw XmlException() if the method fails.
 */
void Xml::serializeXml() const
{
    const int indent = 4;
    QString fileName(getDataFileName());
    QFile xmlFile(fileName);
    if (!xmlFile.open(QIODevice::WriteOnly))
        throw XmlException(QObject::tr("Could not write to file %1").arg(fileName));
    QTextStream out(&xmlFile);
    mDoc->save(out, indent);
    qDebug() << "Save xml to" << xmlFile.fileName();
}

/*!
    \brief Deserialize the Xml coded data from the file system.
    \details Once this method successfully returns, the Xml coded
    data in it has been serialized from a file.  The file is stored
    based on the type and subtype, and the name - these values must
    be correctly set and identify an existing data item.
    \throw XmlException() if the method fails.
 */
void Xml::deserializeXml()
{
    QString fileName(getDataFileName());
    QFile xmlFile(fileName);
    if (!xmlFile.exists())
        throw XmlFileNotFound(fileName);
    if (!xmlFile.open(QIODevice::ReadOnly))
        throw XmlException(QObject::tr("Could not read from file %1").arg(fileName));
    QString error;
    int line, column;
    if (!mDoc->setContent(&xmlFile, &error, &line, &column))
    {
        xmlFile.close();
        QString message = QObject::tr(
                "Could not set as content file %1, line %2, col %3: %4").arg(
                fileName).arg(line).arg(column).arg(error);
        qDebug() << message;
        throw XmlException(message);
    }
    qDebug() << "Xml::deserializeXml() Loaded xml from" << xmlFile.fileName();
}
